HAOJX

kuberntes开发教程系列(2):了解client-go是什么

字数统计: 3.1k阅读时长: 14 min
2019/09/07 Share

这次说的是如何通过kuberntes API 访问诸如pods , services, deployment等等对象和资源

kuberntes 的编程接口主要由k8s.io/client-go库组成 , client-go是一个典型的web服务客户端库, 它支持REST verbs, 比如: create , get, list ,update,delete,patch , 并且还支持watch

clinet-go在GitHub的地址是https://github.com/kubernetes/client-go , 它对kuberntes版本是一一对应的

prku_0301.png

它的对应表是:

Kubernetes 1.9 Kubernetes 1.10 Kubernetes 1.11 Kubernetes 1.12 Kubernetes 1.13 Kubernetes 1.14 Kubernetes 1.15
client-go 6.0 +- +- +- +- +- +-
client-go 7.0 +- +- +- +- +- +-
client-go 8.0 +- +- +- +- +- +-
client-go 9.0 +- +- +- +- +- +-
client-go 10.0 +- +- +- +- +- +-
client-go 11.0 +- +- +- +- +- +-
client-go 12.0 +- +- +- +- +- +-
client-go HEAD +- +- +- +- +- +- +-

其中

  • ✓ 表示clinet-go和kuberntes都有相同的功能和相同的 API 组版本
  • + 表示clinet-go具有 Kubernetes 集群中可能缺少的特性或 API 组版本。 这可能是因为在 client-go 中添加了功能,或者因为 Kubernetes 删除了旧的、不推荐的功能。 但是,它们的所有共同点(即大多数 api)都可以工作
  • - 表示clinet-go与kuberntes不兼容

从上表看client-go 1.0为 Kubernetes 1.4发布,现在为 Kubernetes 1.15发布了 client-go 12.0 , 看规律是相差了3

API Machinery

还有一个库也是kuberntes开发中经常用到的, 就是API Machinery , 它是实现了kuberntes api所有通过构建块

prku_0303.png

创建和使用client

下面的代码演示了如何使用client-go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (    
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/kubernetes"
)
kubeconfig = flag.String("kubeconfig", "~/.kube/config", "kubeconfig file")
flag.Parse()
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
if err != nil {
fmt.Printf("The kubeconfig cannot be loaded: %v\n", err)
os.Exit(1)
}
clientset, err := kubernetes.NewForConfig(config)
pod, err := clientset.CoreV1().Pods("book").Get("example", metav1.GetOptions{})
  • 我们从clientcmd.BuildConfigFromFlags读取和解析kubeconfig

  • clientcmd.BuildConfigFromFlags中 ,我们得到一个rest.Config , 什么是rest.config,你可以从k8s.io/client-go/rest这个包里看到

  • rest.Config传递给kubernetes.NewForConfig ,这是为了得到一个实际的Kubernetes client set, 它之所以叫client set , 是因为它包含了kuberntes资源的多种client

  • 当我们在集群内运行pod的时候, kubelet将自动的把/var/run/secrets/kubernetes.io/serviceaccount挂载为一个service account, 它取代了刚刚提到的kubecofig file, 并且由rest.InClusterConfig()这个方法转化成了rest.Config,你经常会发现以下 rest.InClusterConfig ()和 clientcmd 的组合。 包括对 KUBECONFIG 环境变量的支持:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    config, err := rest.InClusterConfig()
    if err != nil {
    // fallback to kubeconfig
    kubeconfig := filepath.Join("~", ".kube", "config")
    if envvar := os.Getenv("KUBECONFIG"); len(envvar) >0 {
    kubeconfig = envvar
    }
    config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
    if err != nil {
    fmt.Printf("The kubeconfig cannot be loaded: %v\n", err)
    os.Exit(1)
    }
    }
  • 之后我们选择core组v1 version 去一个叫examle的namespace中访问一个名为book的pod, 即以下代码

    1
    pod, err := clientset.CoreV1().Pods("book").Get("example",metav1.GetOptions{})

    上述代码中,我们用metav1的getoptions方法去访问API Server , 其中corev1 , pods ,namespa这些变量也随之get调用送到了server (这通常被叫做为builder pattern)

    之后这个Get call用HTTP GET方法去请求/api/v1/namespaces/book/pods/example , 如果server回应了200响应码 , 那么在response中会携带经过编码的pod对象 , 或者是json

Kubernetes Objects

从系统的角度来说, kuberntes用go interface实现了kuberntes objects , 它被叫做runtime.Object

它定义在k8s.io/apimachinery/pkg/runtime , 实际上看上去很简单

1
2
3
4
5
6
7
8
9
10
// Object interface must be supported by all API types registered with Scheme.
// Since objects in a scheme are expected to be serialized to the wire, the
// interface an Object must provide to the Scheme allows serializers to set
// the kind, version, and group the object is represented as. An Object may
// choose to return a no-op ObjectKindAccessor in cases where it is not
// expected to be serialized.
type Object interface {
GetObjectKind() schema.ObjectKind
DeepCopyObject() Object
}

其中schema.ObjectKind(from the k8s.io/apimachinery/pkg/runtime/schema package)也是一个简单的接口

1
2
3
4
5
6
7
8
9
10
11
// All objects that are serialized from a Scheme encode their type information.
// This interface is used by serialization to set type information from the
// Scheme onto the serialized version of an object. For objects that cannot
// be serialized or have unique requirements, this interface may be a no-op.
type ObjectKind interface {
// SetGroupVersionKind sets or clears the intended serialized kind of an // object. Passing kind nil should clear the current setting.
SetGroupVersionKind(kind GroupVersionKind)
// GroupVersionKind returns the stored group, version, and kind of an
// object, or nil if the object does not expose or provide these fields.
GroupVersionKind() GroupVersionKind
}

也就是说kuberntes object在go中是一个数据结构

  • 返回并设置GroupVersionKind

  • deep-copied 深度复制

    深层副本是数据结构的克隆,它不与原始对象共享任何内存。 只要代码必须在不修改原始对象的情况下修改对象,就可以使用它

TypeMeta

从上面我们可以看到runtime.Object就是一个go interface , 通过kuberntes/apimachinery/pkg/apis/meta/v1/types.go中定义的TypeMeta镶嵌进kuberntes类型中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// TypeMeta describes an individual object in an API response or request
// with strings representing the type of the object and its API schema version.
// Structures that are versioned or persisted should inline TypeMeta.
//
// +k8s:deepcopy-gen=false
type TypeMeta struct {
// Kind is a string value representing the REST resource this object represents.
// Servers may infer this from the endpoint the client submits requests to.
// Cannot be updated.
// In CamelCase.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
// +optional
Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"`

// APIVersion defines the versioned schema of this representation of an object.
// Servers should convert recognized schemas to the latest internal value, and
// may reject unrecognized values.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
// +optional
APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,2,opt,name=apiVersion"`
}

一个pod的类型声望看起来像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Pod struct {    
metav1.TypeMeta `json:",inline"`
// Standard object's metadata.
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`
// Specification of the desired behavior of the pod.
// +optional
Spec PodSpec `json:"spec,omitempty"`
// Most recently observed status of the pod.
// This data may not be up to date.
// Populated by the system.
// Read-only.
// +optional
Status PodStatus `json:"status,omitempty"`
}

ObjectMeta

除了 TypeMeta 之外,大多数顶级对象都有一个 metav1.ObjectMeta 类型的字段,同样来自于 k8s.io/apimachinery/pkg/meta/v1文件包

1
2
3
4
5
6
7
8
9
10
11
type ObjectMeta struct {    
Name string `json:"name,omitempty"`
Namespace string `json:"namespace,omitempty"`
UID types.UID `json:"uid,omitempty"`
ResourceVersion string `json:"resourceVersion,omitempty"`
CreationTimestamp Time `json:"creationTimestamp,omitempty"`
DeletionTimestamp *Time `json:"deletionTimestamp,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
...
}

在yaml文件的编写过程中,他们对应的是metadata字段

1
2
3
metadata:  
- namespace: default
- name: example

spec和status

最后,几乎每个顶级对象都有 spec 和 status 部分。 这个约定来自 Kubernetes API 的声明性质: spec 是用户的愿望,而 status 是这个愿望的结果

Informers and Caching

上面的clinet set也包括了watch方法,它提供了一个对对象的更改(添加、删除、更新)做出反应的事件接口 , informer提供了更高级的接口, 农村缓存和索引 , 这个为了减轻 api服务器的压力

informers的工作逻辑:

  • 从API Server获取输入
  • 用类似与client 接口相似的lister从内存中获取和列出对象
  • 为add,removes,updates注册event handler
  • 用strore这种结构实现内存缓存

每种特定的类型都会有特定的informer 也就是说每个GVR都会有对应的informer , 为了更容易的共享informer, 我们用shared informer factory来实例化shared informer , shared informer可以允许我们能共享同一类资源, 在实际开发中我们用shared informer factory来初始化informer

从一个REST config开始, 我们可以很容易用client set来创建一个shared informer factory ,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (    
...
"k8s.io/client-go/informers"
)
...
clientset, err := kubernetes.NewForConfig(config)
informerFactory := informers.NewSharedInformerFactory(clientset, time.Second*30)
podInformer := informerFactory.Core().V1().Pods()podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(new interface{}) {...},
UpdateFunc: func(old, new interface{}) {...},
DeleteFunc: func(obj interface{}) {...},})
informerFactory.Start(wait.NeverStop)
informerFactory.WaitForCacheSync(wait.NeverStop)
pod, err := podInformer.Lister().Pods("programming-kubernetes").Get("client-go")

上面展示了如何如何创建一个pod的informer

另外informer还可以添加几个事件(add , update delete)的handler , 这些通常是为了触发控制器的业务逻辑(这个之后再讲)

其实informer是控制器循环的重中之重, 这里只是先简单介绍下, 之后我会再很详细的说明, 这里先有个概念即可

Work Queue

工作队列是一种数据结构, 你可以按照队列预定义的顺序添加元素和从队列中提取元素 , 这种队列被称为priority queue , client-go用k8s.io/client-go/util/workqueue来实现

实现的接口如下:

1
2
3
4
5
6
7
8
type Interface interface {    
Add(item interface{})
Len() int
Get() (item interface{}, shutdown bool)
Done(item interface{})
ShutDown()
ShuttingDown() bool
}

上述中Add(item)会添加一个项, 之后Len()会获取其长度, Get ()返回一个具有最高优先级的项(并阻塞直到有一个可用)。 Get ()返回的每个项在控制器完成处理后都需要一个 Done (item)调用。 同时,一个重复的 Add (item)只会将该项标记为 dirty,以便在调用 Done (item)时重新读取该项

下面的队列类型是从这个通用接口派生的

Delayinginterface 可以在以后添加一个项。 这使得在失败之后重新查找项目变得更加容易,而不会陷入热循环

1
2
3
4
5
6
type DelayingInterface interface {
Interface
// AddAfter adds an item to the workqueue after the
// indicated duration has passed.
AddAfter(item interface{}, duration time.Duration)
}

Ratelimitinginterface 速率限制添加到队列中的项。 它扩展了 DelayingInterface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type RateLimitingInterface interface {
DelayingInterface

// AddRateLimited adds an item to the workqueue after the rate
// limiter says it's OK.
AddRateLimited(item interface{})

// Forget indicates that an item is finished being retried.
// It doesn't matter whether it's for perm failing or success;
// we'll stop the rate limiter from tracking it. This only clears
// the `rateLimiter`; you still have to call `Done` on the queue.
Forget(item interface{})

// NumRequeues returns back how many times the item was requeued.
NumRequeues(item interface{}) int
}

深入API Machinery

API Machinery是核心就是kind

API machine 库中的一个核心术语是 GroupVersionKind,简称 GVK

在go中,每个 GVK 对应一种go type。 相比之下,Go type可以属于多个 gvk

kind的格式通常是单数 , 对于 CustomResourceDefinition 类型,它必须是 DNS 路径标签(RFC 1035)

每个 GVR 对应一个 HTTP 路径。 Gvrs 用于标识 kubernettes API 的 REST 端点, 比如GVR apps/v1.deployments映射到/apis/apps/v1/namespaces/namespace/deployments

client使用此映射构造 HTTP 路径来访问 GVR

如何分辨一个资源是集群范围的还是名称空间中的呢

在HTTP路径上有namespace的就是属于名称空间内的, 如果没有就是属于集群范围内的 ,比如rbac.authorization.k8s.io/v1.clusterroles就是集群范围内的 , 可以在apis/rbac.authorization.k8s.io/v1/clusterroles中访问到

资源通常是用小写和复数表示, 而且必须符合DNS路径规范(RFC 1025)

###REST Mapping

将GVK映射到GVR的过程叫做REST mapping

RESTMapper是个Golang interface, 它的作用就是通过GVK请求GVR

1
RESTMapping(gk schema.GroupKind, versions ...string) (*RESTMapping, error)
1
2
3
4
5
6
7
8
9
10
11
12
type RESTMapping struct {
// Resource is the GroupVersionResource (location) for this endpoint.
Resource schema.GroupVersionResource.

// GroupVersionKind is the GroupVersionKind (data format) to submit
// to this endpoint.
GroupVersionKind schema.GroupVersionKind

// Scope contains the information needed to deal with REST Resources
// that are in a resource hierarchy.
Scope RESTScope
}

此外这个接口还提供许多便利的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// KindFor takes a partial resource and returns the single match.
// Returns an error if there are multiple matches.
KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error)

// KindsFor takes a partial resource and returns the list of potential
// kinds in priority order.
KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error)

// ResourceFor takes a partial resource and returns the single match.
// Returns an error if there are multiple matches.
ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error)

// ResourcesFor takes a partial resource and returns the list of potential
// resource in priority order.
ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error)

// RESTMappings returns all resource mappings for the provided group kind
// if no version search is provided. Otherwise identifies a preferred resource
// mapping for the provided version(s).
RESTMappings(gk schema.GroupKind, versions ...string) ([]*RESTMapping, error)

Restmapper 接口有多个不同的实现。 对于客户端应用程序来说,最重要的一个功能就是 k8s.io/client-go/restmapper 包中基于发现的 DeferredDiscoveryRESTMapper: 它使用来自 Kubernetes API 服务器的发现信息来动态地构建 REST 映射。 它还可以与非核心资源(如定制资源)一起工作。

Scheme

最后一个概念就是来自k8s.io/apimachinery/pkg/runtime中的scheme , 它可以将kuberntes中的GVK这些概念联系起来 , 它的作用就是将go type映射到GVK中

1
func (s *Scheme) ObjectKinds(obj Object) ([]schema.GroupVersionKind, bool, error)

在上面的kuberntes objects小节中说了 , 一个object有个属性就是GetObjectKind() , 类型是schema.ObjectKind

该包可以通过反射获取给定对象的 Golang 类型,并将其映射到已注册的 Golang 类型的 GVK中,

使用以下的函数可以达成目的

1
scheme.AddKnownTypes(schema.GroupVersionKind{"", "v1", "Pod"}, &Pod{})

当然对于kuberntes核心的类型还说, schema已经注册好了已经有的类型在client-go client set中预先注册好的类型 , 实际上每个由client-gen code generator生成的client set中都有一个所有type所有group所有version的一个子包

总结一下, api machinery包的概念和作用是如下所示

prku_0307.png

CATALOG
  1. 1. API Machinery
  2. 2. 创建和使用client
  3. 3. Kubernetes Objects
  4. 4. TypeMeta
  5. 5. ObjectMeta
  6. 6. spec和status
  7. 7. Informers and Caching
  8. 8. Work Queue
  9. 9. 深入API Machinery
  10. 10. Scheme